VHID daemon: ProcessType=Interactive to prevent load-induced disconnects (MAL-57 Layer 1)#882
Conversation
Review: VHID daemon ProcessType=Interactive (MAL-57 Layer 1)The root-cause analysis is thorough and the fix is well-targeted. A few observations: What's done well
One meaningful gap (acknowledged, but worth flagging explicitly)
The effect: users who installed before this PR keep the stuck-key bug unless they happen to trigger a VHID repair for a different reason. The PR doc notes this as a follow-up; I'd suggest tracking it as a distinct issue. Connecting Minor: verbose doc commentsCLAUDE.md style calls for a single short WHY comment when the constraint is non-obvious. The doc block added to Same applies to the test doc comment ( Not a blockerThe duplicate plist generators ( Overall: Correct and safe to merge. The proactive-repair gap is worth a follow-up issue so it doesn't stay theoretical. |
…cts (MAL-57 Layer 1) Under heavy CPU load the Karabiner VHID daemon can be starved long enough to miss the pqrs client's 3s heartbeat. Kanata then drops its output connection mid-keystroke, losing key Release events and leaving the virtual keyboard's key stuck down — macOS autorepeats it for ~3 seconds (multi-letter bursts like 'togggggg'). The shipped SMAppService kanata plist already sets ProcessType=Interactive; the KeyPath-installed VHID daemon plist set no ProcessType at all, so the process that must answer heartbeats and write key reports was the one launchd could deprioritize. Changes: - Add ProcessType=Interactive to the VHID daemon plist in both generator copies (PlistGenerator + privileged helper), the legacy kanata plist generators (for consistency with the shipped SMAppService plist), and the debug script template. - isVHIDDaemonConfiguredCorrectly() now also requires the key, so a pre-fix plist reports misconfigured and repair rewrites it. - Tests: plist generation asserts the key; health check covers the missing-ProcessType case. - Document the 2026-06-10 live incident evidence in the MAL-57 bug doc: full causal chain across kanata, the VHID daemon, and DriverKit, plus the remaining fix layers for future work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
31b2847 to
46a5941
Compare
Code ReviewPR: VHID daemon: ProcessType=Interactive to prevent load-induced disconnects (MAL-57 Layer 1) OverviewA clean, targeted fix for a real production bug. The causal chain (CPU load → heartbeat miss → connection drop → stuck Release → autorepeat burst) is now documented with strong empirical evidence and fixed at the right layer. The code change is minimal and applied consistently. Strengths
Issues1. Missing test: wrong ProcessType value (minor)
func testIsVHIDDaemonConfiguredCorrectlyReturnsFalseWithWrongProcessType() throws {
let expectedPath = "...Karabiner-VirtualHIDDevice-Daemon"
try writeVHIDPlist(programPath: expectedPath, processType: "Background")
XCTAssertFalse(checker.isVHIDDaemonConfiguredCorrectly())
}2. The PR note says:
This is the right call for scope control, but it means a user on an old install will have the wrong plist until they run repair. The bug is load-triggered and intermittent, so affected users may not file a ticket. Worth opening a follow-up issue if one does not exist. 3. From Nit
Since you're editing this file, worth fixing the link to a repo-relative path while you're here. SummaryThe core fix is correct and consistently applied. Tests use the right parsing approach. The only actionable item before merge is the missing wrong-value |
Summary
Two live stuck-key incidents on 2026-06-10 (multi-second autorepeat bursts while typing) were traced end to end: under heavy CPU load the Karabiner VHID daemon misses the pqrs client's 3s heartbeat, kanata's output connection dies mid-keystroke, in-flight key Release events are lost, and macOS autorepeats the stuck key until the daemon tears down the old client (~3s). Full evidence and causal chain are documented in
docs/bugs/MAL-57-duplicate-keypresses.md(new "2026-06-10 Incident Evidence" section).Root-cause asymmetry: the shipped SMAppService kanata plist already sets
ProcessType=Interactive, but the KeyPath-installed VHID daemon plist set noProcessType— so launchd could deprioritize exactly the process that must answer heartbeats and write key reports.Changes
ProcessType=Interactiveto the VHID daemon plist in both generator copies (PlistGenerator.generateVHIDDaemonPlist()+ the privileged helper's duplicate), the legacy kanata plist generators (consistency with the shipped SMAppService plist), and the debug script templateisVHIDDaemonConfiguredCorrectly()now also requires the key, so a pre-fix plist reports misconfigured and repair rewrites itTest plan
./Scripts/test-full.shgreen on the final codePlistGeneratorTests(27 tests) andServiceHealthCheckerTestsgreen, including newtestGenerateVHIDDaemonPlistUsesInteractiveProcessTypeandtestIsVHIDDaemonConfiguredCorrectlyReturnsFalseWithoutProcessTypeoutput backend unavailable during write/connectedreconnects in/var/log/com.keypath.kanata.stdout.logbefore vs after, under similar build load